#include <mc/loader.h>
#include <mc/world.h>
#include <mc/types.h>
#include <mc/chunk.h>
#include <math/noise.h>
#include <util/singleton.h>
#include <fstream>
#include <string>
#include <iostream>

namespace mc
{
static World* world = util::Singleton<World>::instance();

Loader* Loader::_instance = nullptr;

Loader* Loader::instance(const std::string& filename)
{
    if (Loader::_instance == nullptr)
        Loader::_instance = new Loader(filename);
    return Loader::_instance;
}

void Loader::free()
{
    delete Loader::_instance;
    Loader::_instance = nullptr;
}

// 区块数据偏移与区块大小
constexpr unsigned int BIAS = 2 * sizeof(int64_t) + 3 * sizeof(float);
constexpr unsigned int CHUNK_BYTE = 2 * sizeof(int) + CHUNK_SZIE * CHUNK_SZIE * CHUNK_HEIGHT * sizeof(BlockType);

//! @brief 构造函数
Loader::Loader(const std::string& filename): 
    _file(), _filename(filename), 
    _exit(std::ifstream(filename).good())
{
#ifdef DEBUG
    if (this->_exit == false)
        std::cout << "file: " << filename << " not exit\n"; 
#endif
    if (this->_exit)
        this->_file.open(filename, std::ios::binary | std::ios::in | std::ios::out);
    else 
        this->_file.open(filename, std::ios::binary | std::ios::out);
}

//! @brief 析构函数
Loader::~Loader() noexcept
{
#ifdef DEBUG
    std::cout << "free loader\n";
#endif
    this->_file.close();
}

//! @brief 从文件加载世界
glm::vec3 Loader::load()
{
    // 如果文件不存在，先创建一张地图
    if (this->_exit == false)
        return this->create();

    this->_file.seekg(0, std::ios::beg);

    // 地图种子
    this->_file.read(reinterpret_cast<char*>(&world->_seed), sizeof(world->_seed));

    // 区块数量
    uint64_t chunkCnt = 0;
    this->_file.read(reinterpret_cast<char*>(&chunkCnt), sizeof(chunkCnt));

    // 玩家位置
    float x = 0.0f, y = 0.0f, z = 0.0f;
    this->_file.read(reinterpret_cast<char*>(&x), sizeof(float));
    this->_file.read(reinterpret_cast<char*>(&y), sizeof(float));
    this->_file.read(reinterpret_cast<char*>(&z), sizeof(float));

    // 玩家所在区块 id
    ChunkID currentId = Chunk::getChunkID(x, z);

    // 依次遍历所有的区块
    for (uint64_t i = 0; i < chunkCnt; ++i)
    {
        this->_file.seekg(BIAS + i * CHUNK_BYTE, std::ios::beg);
        ChunkID id(0, 0);
        this->_file.read(reinterpret_cast<char*>(&id.id), sizeof(ChunkID));
#ifdef DEBUG
        std::cout << "No.(" << id.coord.x << " ," << id.coord.y << ") chunk\n";
#endif
        world->_chunkMap[id] = ChunkMsg(i, nullptr);
    }

    // 遍历所有加载区域内的方块
    unsigned int width = world->_radius * 2 + 1;
    for (int i = 0; i < width; ++i)
        for (int j = 0; j < width; ++j)
        {
            ChunkID id(currentId.coord.x + i - world->_radius, currentId.coord.y + j - world->_radius);
            this->loadChunk(id);
        }

#ifdef DEBUG
    std::cout << "total " << chunkCnt << " chunks, stored " << world->_chunkMap.size() << ", load " << world->_chunks.size() << '\n';
#endif

    return glm::vec3(x, y + 1.0f, z);
}

//! @brief 创建空世界
glm::vec3 Loader::create()
{
    // 生成种子
    world->_seed = static_cast<uint64_t>(rand()) & 0x00000000ffffffff;
    world->_seed |= (static_cast<uint64_t>(rand()) << 32);

    // 生成随机区块
    ChunkID id(rand() % CHUNK_SZIE, rand() % CHUNK_SZIE);
    
    // 玩家位置
    glm::vec3 pos(id.coord.x * CHUNK_SZIE + (rand() % (CHUNK_SZIE / 2)) + 0.5f, 
                  60, 
                  id.coord.y * CHUNK_SZIE + (rand() % (CHUNK_SZIE / 2)) + 0.5f);

    // 初始化区块
    unsigned int width = world->_radius * 2 + 1;
    for (int i = 0; i < width; ++i)
        for (int j = 0; j < width; ++j)
        {
            ChunkID cid(id.coord.x + i - world->_radius, id.coord.y + j - world->_radius);
            this->initChunk(cid);
        }

    // 保存磁盘
    this->save(pos);

    // 关闭文件，再以读写方式重写打开
    this->_file.close();
    this->_file.open(this->_filename, std::ios::binary | std::ios::in | std::ios::out);

    return pos;
}

//! @brief 保存世界到文件
void Loader::save(const glm::vec3& pos)
{
#ifdef DEBUG
    std::cout << "total " << world->_chunkMap.size() << " chunks, need to save " << world->_chunks.size() << '\n';
#endif
    // 地图种子
    this->_file.seekp(0, std::ios::beg);
    this->_file.write(reinterpret_cast<const char*>(&world->_seed), sizeof(world->_seed));

    // 区块数量
    uint64_t chunkCnt = world->_chunkMap.size();
    this->_file.write(reinterpret_cast<const char*>(&chunkCnt), sizeof(chunkCnt));

    // 保存玩家位置
    this->_file.write(reinterpret_cast<const char*>(&pos.x), sizeof(float));
    this->_file.write(reinterpret_cast<const char*>(&pos.y), sizeof(float));
    this->_file.write(reinterpret_cast<const char*>(&pos.z), sizeof(float));

    // 保存地图方块数据，只要保存在 chunks 中的
    for (auto iter = world->_chunks.begin(); iter != world->_chunks.end(); ++iter)
    {
        Chunk* chunk = *iter;
        uint64_t index = (world->_chunkMap.find(chunk->_id))->second.index;
        this->_file.seekp(BIAS + index * CHUNK_BYTE, std::ios::beg);

        // 区块号
        this->_file.write(reinterpret_cast<const char*>(&(chunk->_id)), sizeof(ChunkID));
        
        // 数据
        for (unsigned int x = 0; x < CHUNK_SZIE; ++x)
            for (unsigned int y = 0; y < CHUNK_HEIGHT; ++y)
                this->_file.write(reinterpret_cast<const char*>(chunk->_blockData[x][y].data()), sizeof(BlockType) * CHUNK_SZIE);
    }
}

//! @brief 保存特定区块
void Loader::saveChunk(const ChunkID& id)
{
    // 获取区块信息
    auto iter = world->_chunkMap.find(id);
    ChunkMsg& msg = iter->second;

    // 写入区块号
    this->_file.seekp(BIAS + msg.index * CHUNK_BYTE, std::ios::beg);
    this->_file.write(reinterpret_cast<const char*>(&(id)), sizeof(ChunkID));

    // 写入区块数据
    for (unsigned int x = 0; x < CHUNK_SZIE; ++x)
        for (unsigned int y = 0; y < CHUNK_HEIGHT; ++y)
        {
            this->_file.write(reinterpret_cast<const char*>(msg.chunk->_blockData[x][y].data()), 
                                CHUNK_SZIE * sizeof(BlockType));
        }

    // 系统参数修改
    world->_chunks.erase(msg.chunk);
    delete msg.chunk;
    msg.chunk = nullptr;
}

//! @brief 加载区块 
Chunk* Loader::loadChunk(const ChunkID& id)
{
    // 在系统中查找区块号
    auto iter = world->_chunkMap.find(id);
    if (iter == world->_chunkMap.end())
        // 不存在的区块！
        return this->initChunk(id);

    // 从内存中读
    ChunkMsg& msg = iter->second;
    if (msg.chunk != nullptr)
        return msg.chunk;

    // 创建新区块，添加到系统中
    Chunk* chunk = new Chunk(id);
    world->_chunks.insert(chunk);
    msg.chunk = chunk;

    // 读取方块信息
    this->_file.seekg(BIAS + msg.index * CHUNK_BYTE + sizeof(ChunkID), std::ios::beg);
    for (unsigned int x = 0; x < CHUNK_SZIE; ++x)
        for (unsigned int y = 0; y < CHUNK_HEIGHT; ++y)
            for (unsigned int z = 0; z < CHUNK_SZIE; ++z)
            {
                char* ptr = reinterpret_cast<char*>( &(chunk->_blockData[x][y][z]) );
                this->_file.read(ptr, sizeof(BlockType));
                BlockType type = chunk->_blockData[x][y][z];
                if (type != BlockType::EMPTY)
                    chunk->placeBlock(x, y, z, type, false);
            }

    // 更新区块的实例化矩阵
    chunk->updatePosition();
    
    return chunk;
}

//! @brief 初始化区块
Chunk* Loader::initChunk(const ChunkID& id)
{
    Chunk* chunk = new Chunk(id);

    // 区块的基础坐标
    int64_t baseX = id.coord.x * CHUNK_SZIE;
    int64_t baseZ = id.coord.y * CHUNK_SZIE;

    // 连接到系统
    world->_chunks.insert(chunk);
    world->_chunkMap[id] = ChunkMsg(world->_chunkMap.size(), chunk);

    // 石头一层
    for (int64_t x = 0; x < CHUNK_SZIE; ++x)
        for (int64_t y = 0; y < 16; ++y)
            for (int64_t z = 0; z < CHUNK_SZIE; ++z)
                chunk->placeBlock(x, y, z, BlockType::STONE, false);

    math::Noise noise(world->_seed, baseX, baseZ, CHUNK_SZIE);

    for (int64_t x = 0; x < CHUNK_SZIE; ++x)
        for (int64_t z = 0; z < CHUNK_SZIE; ++z)
        {
            float val = noise.valueNoise(x, z);
            for (int64_t y = 16; y < 16 + 6 + static_cast<int64_t>(5 * val); ++y)
                chunk->placeBlock(x, y, z, BlockType::DIRT, false);
            chunk->placeBlock(x, 16 + 6 + static_cast<int64_t>(5 * val), z, BlockType::GRASS, false);
        }
    
    chunk->updatePosition();
    return chunk;
}

}